#!/usr/bin/env python

import os, sys, re

# Just to get rid of DeprecationWarings about Cookie class
#import warnings
#warnings.filterwarnings ( "ignore", ".* class is insecure.*", DeprecationWarning )

from os3.dbm.dbmanager import DBManager
from os3.utils.simplejson.encoder import JSONEncoder
from os3.utils.cache_man import CacheHandler
from os3.types.dotdict import dotdict

from layout_manager import LayoutManager
from utils import LiWEUtils
from mailer import LiWEMailer

try:
	import pylibmc
except:
	pass


# {{{ LiWEConfig - A class to handle site-specific configuration
class LiWEConfig ( object ):
	"""
	Valid keys:

		- DB_HOST		- Host for the database
		- DB_KIND		- Type of database
			- adodb
			- fulcrum
			- mysql
			- oracle
			- postgresql
			- sqlite
			- sqlserver
		- DB_NAME		- Database name
		- DB_PASSWORD		- Password to access the database
		- DB_PREFIX		- Database prefix
		- DB_USER		- Login to access the database

		- GZIP_CONTENT		- Flag [ 0 / 1 ] to GZip HTML content. [ 1 ]
		- SESSION_NAME		- Session name for cookie. [ LIWESESS ]
	"""
	def __init__ ( self, base_dir, cfg_name = "" ):
		self.attrs = {}

		try:
			self._read_yaml ( base_dir )
			self.attrs = dotdict ( self.attrs )
		except:
			raise
			self.name = os.path.join ( base_dir, cfg_name )
			self._read ()

	def __getitem__ ( self, key ):
		return self.attrs.get ( key, None )

	def __setitem__ ( self, key, val ):
		self.attrs [ key ] =  val

	def get ( self, key, def_val = None ):
		return self.attrs.get ( key, def_val )


	def has_key ( self, key ):
		return self.attrs.has_key ( key )

	def _read_yaml ( self, base_path ):
		import glob
		import yaml

		def _get_yaml ( base_path, path, mode, res ):
			for g in glob.glob ( os.path.join ( base_path, path ) ):
				name = os.path.basename ( g ).replace ( ".yaml", "" )
				try:
					dct = yaml.load ( open ( g ) )
					if mode == 'filename':
						dest = res.setdefault ( name, {} )
						dest.update ( dct )
					else:
						res.update ( dct )
				except:
					sys.stderr.write ( "ERROR parsing: %s\n" % g )

		# Read the default "liwe.yaml" file
		# FABIO: new: now tabs are converted to 8 spaces automatically
		dct = yaml.load ( open ( os.path.join ( base_path, "liwe.yaml" ) ).read ().replace ( "\t", "        " ) )
		self.attrs.update ( dct )

		if not "include" in self.attrs: return

		for mod in self.attrs [ 'include' ].keys ():
			mod = self.attrs [ 'include' ] [ mod ]
			dest = self.attrs.setdefault ( mod [ 'key' ], {} )
			_get_yaml ( base_path, mod [ 'path' ], mod.get ( 'mode', 'filename' ), dest )

	def _read ( self ):
		try:
			f = open ( self.name )
		except:
			sys.stderr.write ( "WARN: could not read file: '%s'\n" % self.name )
			return

		for l in f:
			l = l.strip ()

			if not l: continue
			if l [ 0 ] == "#": continue

			spl = l.split ( "=" )
			self.attrs [ spl [ 0 ].strip () ] = spl [ 1 ]

		f.close ()
# }}}
# {{{ liwe - The main liwe class
class liwe ( object ):
	def __init__ ( self, pyhp = None, cfg = "liwe.cfg", home_dir = '.', debug = 0 ):
		self._fixio ()

		self.home_dir = home_dir
		self._dbm = None
		self.debug = debug
		self.pyhp = pyhp
		self.env  = pyhp.env

		self.base_url = self._base_url ()

		if "DOCUMENT_ROOT" in self.env:
			sys.path.append ( self.env [ "DOCUMENT_ROOT" ] )

		self.uri_data = self._uri_parser ()

		#self.pyhp.set_callback ( "end", self._end )

		self.cfg = LiWEConfig ( home_dir, cfg )

		self.layman = LayoutManager ( self )
		self.utils  = LiWEUtils ( self )
		self.mail   = LiWEMailer ( self )

		try:
			self.memc = pylibmc.Client ( [ self.cfg.get ( "memcache.server", "127.0.0.1" ) ], binary = True )
		except:
			self.memc = None

		# Array of DBManagers
		self._dbms = {}

		self.err_code = 0
		self.err_descr = ''

		self.data = {}

		self.json = JSONEncoder ()

		self.re = {}
		self.re [ 'safe_match' ] = re.compile ( "^-[ \t]*([a-zA-Z_][a-zA-Z0-9_]*):.*SAFE_([A-Z_]*)" )
		self.re [ 'perms' ] = re.compile ( "^-[ \t]*([a-zA-Z_][a-zA-Z0-9_]*)[ \t:]*.*" )
		self.re [ 'section_start' ] = re.compile ( "^([A-Z][A-Z]*):" )

		self._set_log_level ()
		self._start_session ()

		self._strict_mode = int ( self.cfg.get ( "site.strict_mode", 0 ) )
		#CacheMan
		self.cache = None
		mode = self.cfg.get ( "site.cache.mode" )
		params = self.cfg.get ( "site.cache.params", {} )
		if mode :
			self.cache = CacheHandler.create ( mode, params )
			
	def _start_session ( self ):
		from session import LiWESession

		timeout = int ( self.cfg.get ( "session.timeout", 60 ) )
		domain  = self.cfg.get ( "session.domain", "" )

		handler = LiWESession ( self )

		self.pyhp.session.start ( handler, timeout = timeout, domain = domain )

		self.session = self.pyhp.session

	def _end ( self ):
		return
		self.close ()

	def _set_log_level ( self ):
		dbg = self.cfg.get ( "debug.level", "ERROR" ).upper ()
		if dbg == "DEBUG":
			self.pyhp.log.level = self.pyhp.log.LOG_DEBUG
		elif dbg == "INFO":
			self.pyhp.log.level = self.pyhp.log.LOG_INFO
		elif dbg == "WARN":
			self.pyhp.log.level = self.pyhp.log.LOG_WARN
		elif dbg == "ERROR":
			self.pyhp.log.level = self.pyhp.log.LOG_ERROR
		elif dbg == "CRITICAL":
			self.pyhp.log.level = self.pyhp.log.LOG_CRITICAL

	def _init_dbm ( self, db_id = '' ):
		dbs = self.cfg.get ( "dbm" )
		if not dbs: return self._init_single_dbm ( db_id )

		from  dbm import DBM
		self._dbm = DBM ( self, "mysql" )
		self._dbm.multi_connect ( dbs )

		return self._dbm

	def _init_single_dbm ( self, db_id = '' ):
		if db_id:
			prefix = "modules." + db_id +  "." 
		else:
			prefix = ''

		dbk = self.cfg.get ( '%sdb.kind' % prefix, "" ).lower ()

		real_db = -1
		for db_kind in DBManager.db_types:
			dbt = DBManager.db_types [ db_kind ]
			if dbk == dbt [ 0 ]:
				real_db = db_kind
				break

		if real_db == -1:
			self.pyhp.log.error ( "ERROR: wrong string: '%s'" % dbk )
			return

		db = DBManager ( real_db )
		db.set_prefix ( self.cfg.get ( "%sdb.prefix" % prefix, "" ) )
		params = dict (
			host = self.cfg.get ( "%sdb.host" % prefix, "localhost" ),
			user = self.cfg.get ( "%sdb.user" % prefix, "" ),
			passwd = self.cfg.get ( "%sdb.password" % prefix, "" ),
			db = self.cfg.get ( "%sdb.name" % prefix, "" ),
			charset = 'utf8',
			use_unicode = True,
		)
		sock = self.cfg.get ( "%sdb.socket" % prefix, "" )
		if sock:
			params [ 'unix_socket' ] = sock
		db.connect ( ** params )

		if not db_id:
			self._dbms [ '__default__' ] = db
		else:
			self._dbms [ db_id ] = db

		return db

	def close_dbms ( self ):
		for k, v in self._dbms.iteritems ():
			try:
				v.close ()
			except:
				pass

	def get_dbm ( self, db_id ):
		if not db_id:
			return self.dbm

		db = None

		db_id = str ( db_id ).lower ()
		if db_id not in self._dbms: 
			db = self._init_dbm ( db_id )
		else:
			db = self._dbms [ db_id ]

		return db

	def default_dbm ( self ):
		if not self._dbm:
			self._dbm = self._init_dbm ()

		return self._dbm
	
	dbm = property ( default_dbm, None )

	def get_uid ( self ):
		# FIXME: controllare i permessi e usare la uid solo se puo' gestire gli utenti
		return self.pyhp.session.data.get ( "uid", -1 )

	def get_perms ( self, module_name ):
		uid = self.get_uid ()

		if uid == -1:
			return []

		perms = []
		vals = self.dbm.single_row ( "user_perm", [ 'perms' ], "uid = '%s' AND module_name = '%s'" % ( uid, module_name ) )
		if vals:
			perms.extend ( [ x.lower () for x in vals [ 'perms' ].split ( ":" ) ] )

		
		# the user can be in several groups
		res = self.dbm.select ( [ 'user_group_perm as a', 'user_u2g as b' ], 'a.perms', conds = "b.uid = '%s' AND a.module_name = '%s'" % ( uid, module_name ), joins = 'a.gid = b.gid' )
		if res:
			for row in res:
				perms.extend ( [ x.lower () for x in row [ 'perms' ].split ( ":" ) ] )

		output = []
		for x in perms:
			if x not in output:
				output.append ( x )

		return output

	def module_call ( self, command, dct ):
		"""
		module_call
		-----------

		This method is similar to ``invoke``, but this is the one that should be used when calling an external module method
		from within your code.

		"""
		return self.invoke ( command, ** dct )

	def invoke ( self, command, *args, ** kargs ):
		"""
		LiWE invoke
		===========

		This is the liwe ``invoke()`` method.

		In LiWE *invoke()ing* means load up in memory a single module and executing the desired command (method).

		In LiWE you can specify the command to be executed in one of the following ways:

			- By specifying two attributes:

				- module:	the module to be loaded
				- command:	the command to be executed on the specified ``module``

			- By specifying a *simple* dotted notation, this way:

				- command:	the command in the *simple* dotted notation.
						For example: module.command

			- By specifying a *complete* dotted notation, this way:

				- command:	the command in the *complete* dotted notation.
						For example: modname.file.method

		If you specify a *simple* dotted notation, the ``modname`` and ``file`` will overlap.
		"""
		self.err_code = 0
		self.err_descr = ''

		args = self.__invoke ( command, * args, ** kargs )

		if args == False:
			# An error occurred
			self.pyhp.log.critical ( "CRITICAL: invoke has no args at all" )
		elif isinstance ( args, dict ):
			pass
		elif args == None:
			args = {}
		else:
			args = { "_txt" : "%s" % args }

		args [ 'err_code' ] = self.err_code
		args [ 'err_descr' ] = self.err_descr

		self.err_code = 0
		self.err_descr = ''

		return args

	def close ( self ):
		return
		for name, db in self._dbms.iteritems ():
			db.close ()

		self._dbms = {}

	def __invoke ( self, command, * args, ** kargs ):
		module  = None
		modbase = None
		orig_command = command

		"""
		Se c'e' ``command``, potrebbe essere che stia usando la "dotted notation"
		per invocare il metodo, tipo "modulo.submodulo.metodo", quindi devo provare
		a parsare command.
		"""

		vals = command.split ( "." )

		if len ( vals ) > 1:
			command = vals [ -1 ]

			if len ( vals ) == 2:
				module = '.'.join ( vals [ : -1 ] )
				modbase = vals [ 0 ]
			else:
				modbase = vals [ 0 ]
				module = '.'.join ( vals [ 1 : -1 ] )
		else:
			self.err_code = 1
			self.err_descr = "Module: '%s' not found (command: '%s')" % ( module, command )
			self.pyhp.log.error ( self.err_descr )
			return 


		if not modbase:
			modbase = module

		try:
			mod = __import__ ( "modules.%s.%s" % ( modbase, module ) )
		except:
			self.err_code = 2
			self.err_descr = "Could not find module: '%s' (modules.%s.%s)" % ( module, modbase, module )
			self.pyhp.log.error ( self.err_descr )
			return
	
		# Risolvo il modulo. E' scritto in questo modo: modules.modbase.nome_file
		# Esempio: "modules.test.test"
		mod = getattr ( mod, "%s" % modbase )
		mod = getattr ( mod, "%s" % module )

		try:
			cmd = getattr ( mod, command )
		except:
			self.err_code = 6
			self.err_descr = "Module: (%s/%s) has not command: %s" % ( modbase, module, command )
			self.pyhp.log.error ( self.err_descr )
			return

		return self.command_execute ( modbase, cmd, * args, ** kargs )

	def command_execute ( self, modbase, cmd, * args, ** kargs ):
		cmd_full_name = "%s.%s" % ( cmd.__module__.replace ( "modules.", "" ), cmd.__name__ )

		my_perms = self.get_perms ( modbase )

		if self.__has_perms ( cmd, my_perms ) == False:
			self.err_code = 5
			self.err_descr = "Permission denied for: %s (%s)" % ( cmd_full_name, my_perms )
			self.pyhp.log.error ( self.err_descr )
			return

		# Prendo i parametri della funzione originale
		func_vars = self.__func_vars_info ( cmd )

		# Filtro i parametri di kargs in modo da avere un dizionario
		# che contenga solo le variabili richieste dalla funzione che sto per invocare
		my_kargs = self.__func_prepare_kargs ( cmd, kargs, func_vars )

		try:
			res = cmd ( self, * args, ** my_kargs )
		except:
			self.err_code = 4
			self.err_descr = "Method '%s' failed with an exception." % cmd_full_name
			self.pyhp.log.error ( "ERROR: %s" % self.err_descr )
			raise
			return

		return res

	def __func_vars_info ( self, func ):
		_vars = func.func_code.co_varnames [ : func.func_code.co_argcount ]
		var_names = [ [ x, None ] for x in _vars [ : : -1 ] ]

		defs = func.func_defaults
		if defs:
			defs = defs [ : : -1 ]
			pos = 0
			for x in defs:
				if x == "": x = '""'

				var_names [ pos ] [ 1 ] = x
				pos += 1
		
		res = []
		for f in var_names [ : : -1 ]:
			if f [ 1 ] != None:
				res.append ( "%s = %s" % ( f [ 0 ], f [ 1 ] ) )
			else:
				res.append ( f [ 0 ] )

		return ', '.join ( res )


	def __func_prepare_kargs ( self, cmd, kargs, func_vars ):
		safe_args = self.__prepare_safe_args ( cmd )

		my_kargs = {}
		real_kwargs = {}

		support_kwargs = False

		missing_args = []

		for arg in cmd.func_code.co_varnames [ 1 : cmd.func_code.co_argcount ]:
			if arg == 'kwargs': support_kwargs = True

			if arg in kargs:
				if arg in safe_args:
					safe = safe_args [ arg ]
					self.pyhp.log.debug ( "ARG: %s IN safe: %s" % ( arg, safe ) )
					my_kargs [ arg ] = self.utils.safe_get ( safe [ 0 ], "UPG", safe [ 1 ], safe [ 2 ], kargs [ arg ] )
				else:
					my_kargs [ arg ] = kargs [ arg ]

			if arg not in safe_args:
				missing_args.append ( arg )

		if missing_args: self.__strict_error ( cmd, "missing args in DOCSTRING: %s" % ','.join ( missing_args ), 3 )

		for k in kargs:
			if k not in my_kargs:
				real_kwargs [ k ] = kargs [ k ]

		if "action" in real_kwargs: del real_kwargs [ 'action' ]

		if support_kwargs:
			my_kargs [ 'kwargs' ] = real_kwargs

		return my_kargs

	def __strict_error ( self, func, msg, err_code = 1 ):
		if self._strict_mode:
			msg = "STRICT MODE: %s.%s: %s" % ( func.__module__, func.func_name, msg )
			if self.cfg.get ( "debug.enabled", 0 ):
				self.set_error ( err_code, msg )

			self.pyhp.log.warn ( msg )

	def __prepare_safe_args ( self, cmd ):
		doc = cmd.func_doc
		if not doc:
			self.__strict_error ( cmd, "Missing DOCSTRING in AJAX method", 1 )
			return {}

		doc = [ x.strip () for x in doc.split ( "\n" ) if x.strip () ]
		doc = self.__extract_doc_lines ( doc, "INPUT" )

		if not doc: self.__strict_error ( cmd, "wrong DOCSTRING in AJAX method (missing :INPUT: field)", 2 )

		args = {}
		for d in doc:
			g = self.re [ 'safe_match' ].match ( d )
			if not g:
				continue

			name = g.group ( 1 )
			safe = self.utils.safe_get_mode ( g.group ( 2 ) )
			if d.find ( "[" ) != -1:
				txt = d [ d.find ( "[" ) + 1 : ]
				def_val = txt [ : txt.find ( "]" ) ]
			else:
				def_val = None

			args [ name ] = ( name, safe, def_val )

		self.pyhp.log.debug ( "*** USING SAFE ARGS: %s" % args )

		return args

	def __has_perms ( self, cmd, my_perms ):
		login = self.pyhp.session.data.get ( "login", "no login" )

		if login == 'root':
			return True

		doc = cmd.func_doc
		# If method does not contain PERMS, it is public
		if not doc:
			return True

		doc = [ x.strip () for x in doc.split ( "\n" ) if x.strip () ]
		doc = self.__extract_doc_lines ( doc, "PERMS" )

		if not doc:
			return True

		self.pyhp.log.debug ( "HAS PERMS: my_perms: %s - doc: %s" % ( my_perms, doc ) )

		perms = []
		for d in doc:
			g = self.re [ 'perms' ].match ( d )
			if not g:
				continue

			perm = g.group ( 1 ).lower ()

			if perm in my_perms:
				return True

		return False

	def __extract_doc_lines ( self, doc, main_key ):
		mkey = ":" + main_key + ":"

		pos = 0
		# Search for the beginning of the doc
		for d in doc:
			if d == mkey:
				break
			pos += 1
		else:
			pos = -1

		if pos == -1:
			return []

		end = pos
		for d in doc [ pos +1 : ]:
			end += 1
			if self.re [ 'section_start' ].match ( d ):
				break
		else:
			return doc [ pos : ]

		return doc [ pos : end ]

	def set_error ( self, err_code, err_descr ):
		self.err_code = int ( str ( err_code ), 10 )
		self.err_descr = err_descr

	def _uri_parser ( self ):
		import urllib

		dct = {}

		req_uri = self.env.get ( "REQUEST_URI", "" ) [ 1 : ]
		if req_uri.find ( "/" ) == -1: return dct

		if req_uri.find ( "?" ) != -1:
			req_uri = req_uri [ req_uri.find ( "?" ) + 1 : ]

		parts = [ x for x in req_uri.split ( "/" ) if x ]
		x = 0
		while x < len ( parts ):
			try:
				dct [ parts [ x ] ] = urllib.unquote ( parts [ x + 1 ] )
			except:
				dct [ '__liwe_arg' ] = urllib.unquote ( parts [ x ] )

			x += 2

		return dct

	def _fixio ( self ):
		class my_stderr ( object ):
			def __init__ ( self, stderr ):
				self.stderr = stderr

			def write ( self, txt ):
				try:
					if isinstance ( txt, unicode ):
						txt = txt.encode ( 'ascii', 'backslashreplace' )

					self.stderr.write ( txt )
				except:
					pass

			def flush ( self ): pass

		sys.stderr = my_stderr ( sys.stderr )

	def _base_url ( self ):
		base_url = self.env [ 'wsgi.url_scheme' ] + '://'

		if self.env.has_key ( 'HTTP_HOST' ):
			base_url += self.env [ 'HTTP_HOST' ]
		else:
			base_url += self.env [ 'SERVER_NAME' ]

			if self.env [ 'wsgi.url_scheme' ] == 'https':
				if self.env [ 'SERVER_PORT' ] != '443':
					base_url += ':' + self.env [ 'SERVER_PORT' ]
			else:
				if self.env [ 'SERVER_PORT' ] != '80':
					base_url += ':' + self.env [ 'SERVER_PORT' ]

		return base_url

	def email ( self, sender = None, subject = "(no subject)" ):
		"""
		email ( sender = None, subject = "(no subject)" ) -> Email

		This method creates a new ``Email`` object instance and configures it
		with the right SMTP settings taken from ``liwe.yaml`` config.
		Keys from YAML file are:

			- ``site.email.sender`` - The email sender (if not specified)
			- ``smtp.server``       - The SMTP server address (default: ``localhost`` )
			- ``smtp.port``		- The SMTP server port (default: 25)
			- ``smtp.login``	- The SMTP login (if any) (default: None)
			- ``smtp.password``	- The SMTP password (if any) (default: None)
		"""

		from os3.net.mail import Email

		if not sender:
			sender = self.cfg.get  ( "site.email.sender", "info@example.com" )

		m = Email ( sender = sender, subject = subject )

		m.set_smtp ( server = self.cfg.get ( "smtp.server", "localhost" ),
			     port   = self.cfg.get ( "smtp.port", 25 ),
			     login  = self.cfg.get ( "smtp.login", None ),
			     pwd    = self.cfg.get ( "smtp.password", None )
			)

		return m
# }}}


if __name__ == '__main__':
	l = liwe ()

	#if l.dbm: print l.dbm.table_list ()

	print l.html
